<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>proxyfor</title>
    <link rel="icon" href="data:,">
    <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/stackoverflow-light.min.css">
    <style>
        body,
        div,
        span,
        a,
        button,
        pre,
        code,
        table,
        tbody,
        thead,
        th,
        tr,
        td {
            margin: 0;
            padding: 0;
            border: 0;
            font-size: 100%;
            font: inherit;
            vertical-align: baseline;
        }

        table {
            border-collapse: collapse;
            border-spacing: 0;
        }

        body {
            display: flex;
            height: 100vh;
            padding: 8px;
            margin: 0;
            box-sizing: border-box;
            font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
            font-size: 14px;
            line-height: 1.42857143;
            color: #333;
            background-color: #fff;
        }

        .hidden {
            display: none;
        }

        .left-panel,
        .right-panel {
            display: flex;
            flex-direction: column;
            flex: 1;
            border: 1px solid #ccc;
            width: 50vh;
        }

        .left-panel {
            overflow-y: auto;
            overflow-x: hidden;
        }

        .tabs {
            display: flex;
            border: 1px solid #ccc;
            background-color: #f2f2f2;
        }

        .tab {
            padding: 5px 10px;
            cursor: pointer;
            margin: 2px 2px -1px;
        }

        .tab.active {
            border: 1px solid #ccc;
            background-color: white;
            border-bottom-color: white;
        }

        .toolbox {
            margin-left: auto;
        }

        .dropdown {
            position: relative;
            display: inline-block;
        }

        .dropbtn {
            padding: 8px;
            border: none;
            cursor: pointer;
        }

        .dropdown-content {
            display: none;
            position: absolute;
            cursor: pointer;
            background-color: white;
            right: 0;
            box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
            z-index: 1;
        }

        .dropdown-content a {
            display: block;
            text-decoration: none;
            padding: 3px 10px;
            clear: both;
            font-weight: 400;
            line-height: 1.42857143;
            color: #333;
            white-space: nowrap;
        }

        .dropdown-content a:hover {
            color: #262626;
            text-decoration: none;
            background-color: #f5f5f5;
        }

        .traffic-table {
            width: 100%;
            table-layout: fixed;
            border-collapse: collapse;
            border-spacing: 0;
        }

        .traffic-table th {
            text-align: left;
            background-color: #f2f2f2;
            border-bottom: solid #bebebe 1px;
            font-weight: 400;
            line-height: 23px;
        }

        .traffic-table tr {
            line-height: 30px;
        }

        .traffic-table tr:nth-child(even) {
            background-color: #f2f2f2;
        }

        .traffic-table td {
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }

        .traffic-table tr.selected {
            background-color: #e0ebf5;

        }

        .col-uri {
            padding-left: 4px;
        }

        .col-method {
            width: 70px;
        }

        .col-status {
            width: 50px;
        }

        .col-type {
            width: 60px;
        }

        tbody .col-type {
            font-size: 8px;
            text-align: center;
        }

        .col-size {
            width: 50px;
            text-align: right;
        }

        .col-time {
            width: 50px;
            text-align: right;
            padding-right: 4px;
        }

        .main-view {
            flex: 1;
            border: 1px solid #ccc;
            border-top: none;
            padding: 10px;
            overflow-wrap: break-word;
            overflow-x: hidden;
            overflow-y: auto;
        }

        .firstline {
            font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
            background-color: #428bca;
            color: #fff;
            margin: 0 -8px 2px;
            padding: 4px 8px;
            border-radius: 5px;
            word-break: break-all;
            max-height: 100px;
        }

        .header-line {
            margin-bottom: 0.3em;
            max-height: 12.4ex;
            overflow-y: auto;
        }

        .header-key {
            font-weight: 700;
        }

        .header-value {
            font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
        }

        .code-view {
            display: flex;
            align-items: center;
        }

        .code-view pre {
            display: block;
            padding: 10px;
            margin: 0 0 10px;
            font-size: 13px;
            line-height: 1.42857143;
            color: #333;
            word-break: break-all;
            word-wrap: break-word;
            background-color: #f5f5f5;
            border: 1px solid #ccc;
            border-radius: 4px;
            overflow: auto;
            width: 100%;
        }

        .media-view {
            display: block;
            margin: 0 0 10px;
        }

        .error-view {
            display: block;
            padding: 10px;
            margin: 0 0 10px;
            border: 1px solid transparent;
            border-radius: 4px;
            color: #8a6d3b;
            background-color: #fcf8e3;
            border-color: #faebcc;
        }

        .ws-date {
            float: right;
            font-size: 85%;
        }

        .ws-recv {
            color: #a94442;
        }

        .ws-send {
            color: #337ab7;
        }
    </style>
    <script src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
    <script src="https://unpkg.com/clipboard@2/dist/clipboard.min.js"></script>
</head>

<body>
    <div class="left-panel">
        <table class="traffic-table">
            <thead>
                <tr>
                    <th class="col-uri">Path</th>
                    <th class="col-method">Method</th>
                    <th class="col-status">Status</th>
                    <th class="col-type">Type</th>
                    <th class="col-size">Size</th>
                    <th class="col-time">Time</th>
                </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </div>
    <div class="right-panel">
        <div class="tabs">
            <div data-tab="req" class="tab active">Request</div>
            <div data-tab="res" class="tab">Response</div>
            <div data-tab="ws" class="tab hidden">Websocket</div>
            <div data-tab="err" class="tab hidden">Error</div>
            <div class="toolbox">
                <div class="dropdown export-dropdown">
                    <button class="dropbtn">Export▾</button>
                    <div class="dropdown-content">
                        <a data-kind="markdown">Export all as Markdown</a>
                        <a data-kind="curl">Export all as cURL</a>
                        <a data-kind="har">Export all as HAR</a>
                    </div>
                </div>
                <div class="dropdown copy-dropdown">
                    <button class="dropbtn">Copy▾</button>
                    <div class="dropdown-content">
                        <a data-kind="markdown">Copy as Markdown</a>
                        <a data-kind="curl">Copy as cURL</a>
                        <a data-kind="har">Copy as HAR</a>
                        <a data-kind="res-body">Copy Response Body</a>
                    </div>
                </div>
            </div>
        </div>
        <div class="main-view"></div>
    </div>
    <script>

        /**
         * @typedef {Object} Traffic
         * @property {string} uri
         * @property {string} method
         * @property {?string} req_version
         * @property {?Header[]} req_headers
         * @property {?BodyData} req_body
         * @property {?number} status
         * @property {?Header[]} res_headers
         * @property {?string} res_version
         * @property {?BodyData} res_body
         * @property {?number} websocket_id
         * @property {?string} error
         */

        /**
         * @typedef {Object} TrafficHead
         * @property {number} id
         * @property {string} method
         * @property {string} uri
         * @property {?number} status
         * @property {?number} size
         * @property {?number} time
         * @property {?string} mime
         */

        /**
         * @typedef {Object} Header
         * @property {string} name
         * @property {string} value
         */

        /**
         * @typedef {Object} BodyData
         * @property {"utf8"|"base64"} encode 
         * @property {string} value
         */

        /**
         * @typedef {"markdown"|"curl"|"har"} ExportKind
         */

        /**
         * @typedef {"markdown"|"curl"|"har"|"res-body"} CopyKind
         */

        /**
         * @typedef {Object} WebsocketMessage
         * @property {?string} error
         * @property {?WebsocketData} data
         */

        /**
         * @typedef {Object} WebsocketData
         * @property {create} string
         * @property {bool} server_to_client
         * @property {BodyData} body
         */

        const STATUS_CODES = {
            100: "Continue",
            101: "Switching Protocols",
            200: "OK",
            201: "Created",
            202: "Accepted",
            204: "No Content",
            301: "Moved Permanently",
            302: "Found",
            304: "Not Modified",
            400: "Bad Request",
            401: "Unauthorized",
            403: "Forbidden",
            404: "Not Found",
            405: "Method Not Allowed",
            500: "Internal Server Error",
            502: "Bad Gateway",
            503: "Service Unavailable",
            504: "Gateway Timeout",
        }

        const IMAGE_MIMES = [
            "image/jpeg",
            "image/png",
            "image/gif",
            "image/svg+xml",
            "image/webp",
            "image/bmp",
            "image/x-icon",
        ];

        const AUDIO_MIMES = [
            "audio/mpeg",
            "audio/ogg",
            "audio/wav",
            "audio/webm",
            "audio/aac",
            "audio/flac",
            "audio/x-wav",
            "audio/x-pn-wav",
        ];

        const VIDEO_MIMES = [
            "video/mp4",
            "video/webm",
            "video/ogg",
            "video/quicktime",
            "video/x-msvideo",
            "video/x-flv",
            "video/x-matroska",
        ]

        const BASE_URL = "/__proxyfor__";

        const subscribeTrafficsEndpoint = `${BASE_URL}/subscribe/traffics`;
        const subscribeWebsocketEndpoint = id => `${BASE_URL}/subscribe/websocket/${id}`;
        const getTrafficEndpoint = id => `${BASE_URL}/traffic/${id}`;
        const listTrafficsEndpoint = `${BASE_URL}/traffics`;

        /**
         * @type {HTMLTableElement}
         */
        const $trafficTableBody = document.querySelector(".traffic-table tbody");
        /**
         * @type {HTMLDivElement}
         */
        const $tabs = document.querySelector(".tabs");
        /**
         * @type {HTMLDivElement}
         */
        const $mainView = document.querySelector(".main-view");

        /**
         * @type {HTMLButtonElement}
         */
        const $exportDropbtn = document.querySelector(".export-dropdown .dropbtn");

        /**
         * @type {HTMLDivElement}
         */
        const $exportDropdownContent = document.querySelector(".export-dropdown .dropdown-content");

        /**
         * @type {HTMLButtonElement}
         */
        const $copyDropbtn = document.querySelector(".copy-dropdown .dropbtn");

        /**
         * @type {HTMLDivElement}
         */
        const $copyDropdownContent = document.querySelector(".copy-dropdown .dropdown-content");

        /**
         * @type {AbortController}
         */
        let websocketAbortController = new AbortController();

        /**
         * @type {number}
         */
        let selectedTrafficId = null;

        /**
         * @type {"req" | "res"}
         */
        let selectedTab = "req"

        /**
         * @type {Traffic}
         */
        let currentTraffic = null;


        function handleDOMContentLoaded() {
            fetch(subscribeTrafficsEndpoint)
                .then((res) => {
                    let buf = "";
                    let decoder = new TextDecoder();
                    let reader = res.body.getReader();
                    reader.read().then(function handleNDJson({ value, done }) {
                        if (done) {
                            buf = buf.trim();
                        } else {
                            let data = decoder.decode(value, { stream: true });
                            buf += data;
                        }
                        let lines = buf.split("\n");
                        for (let i = 0; i < lines.length - 1; i++) {
                            const traffic = JSON.parse(lines[i]);
                            handleNewTraffic(traffic);
                        }
                        buf = lines[lines.length - 1];
                        return reader.read().then(handleNDJson);
                    });
                })
                .catch((err) => {
                    console.error("Failed to subscribe traffics", err);
                });

            $trafficTableBody.addEventListener("click", (e) => {
                const $tr = e.target.closest("tr");
                if ($tr) {
                    const id = $tr.getAttribute("data-traffic-id");
                    if (id) handleSelectTraffic(id);
                }
            });
            $tabs.addEventListener("click", (e) => {
                const $tab = e.target.closest(".tab");
                if ($tab) {
                    const tab = $tab.getAttribute("data-tab");
                    if (tab) handleSelectedTab(tab, true);
                }
            });
            $exportDropbtn.addEventListener("click", (e) => {
                $exportDropdownContent.style.display = "block";
                $copyDropdownContent.style.display = "none";
            });
            $exportDropdownContent.addEventListener("click", (e) => {
                $exportDropdownContent.style.display = "none";
                const $kind = e.target.closest("[data-kind]");
                if ($kind) {
                    const kind = $kind.getAttribute("data-kind");
                    if (kind) handleExport(kind);
                }
            });
            $copyDropbtn.addEventListener("click", (e) => {
                $copyDropdownContent.style.display = "block";
                $exportDropdownContent.style.display = "none";
            });
            $copyDropdownContent.addEventListener("click", (e) => {
                $copyDropdownContent.style.display = "none";
                const $kind = e.target.closest("[data-kind]");
                if ($kind) {
                    const kind = $kind.getAttribute("data-kind");
                    if (kind) handleCopy(kind);
                }
            });
            document.addEventListener("click", function (e) {
                if (!e.target.closest(".dropdown")) {
                    $exportDropdownContent.style.display = "none";
                    $copyDropdownContent.style.display = "none";
                }
            });
        }

        function handleSelectTraffic(id) {
            selectedTrafficId = id;
            const $selected = $trafficTableBody.querySelector("tr.selected");
            if ($selected) {
                $selected.classList.remove("selected");
            }
            const $newSelected = $trafficTableBody.querySelector(`tr[data-traffic-id="${id}"]`);
            if ($newSelected) {
                $newSelected.classList.add("selected");
                fetch(getTrafficEndpoint(id))
                    .then(res => res.json())
                    .then(data => {
                        currentTraffic = data;
                    })
                    .catch(err => {
                        currentTraffic = null;
                        console.error(`Failed to fetch traffic ${id}`, err);
                    })
                    .finally(updateTabContent);
            }
        }

        function handleSelectedTab(tab) {
            selectedTab = tab;
            const $active = $tabs.querySelector(".tab.active");
            if ($active) {
                $active.classList.remove("active");
            }
            const $newActive = $tabs.querySelector(`.tab[data-tab="${tab}"]`);
            if ($newActive) {
                $newActive.classList.add("active");
            }
            updateTabContent();
        }

        /**
         * @param {TrafficHead} trafficHead
         */
        function handleNewTraffic(trafficHead) {
            const row = tmplTrafficTableRow(trafficHead);
            $trafficTableBody.insertAdjacentHTML("beforeend", row);
        }

        /**
         * @param {ExportKind} kind
         */
        function handleExport(kind) {
            fetch(listTrafficsEndpoint + `?${kind}`)
                .then(async res => {
                    const contentType = res.headers.get('content-type');
                    const text = await res.text();
                    let filename = 'txt';
                    switch (kind) {
                        case "markdown":
                            filename = 'traffics.md';
                            break;
                        case "curl":
                            filename = 'traffics.sh';
                            break;
                        case "har":
                            filename = 'traffics.har';
                            break;
                    };
                    const file = new File([text], filename, { type: contentType });

                    const url = URL.createObjectURL(file);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = filename;
                    a.click();

                    URL.revokeObjectURL(url);
                })
                .catch(err => {
                    console.error(`Failed to list traffics`, err);
                })
        }

        /**
         * @param {CopyKind} kind
         */
        function handleCopy(kind) {
            if (!currentTraffic) {
                return;
            }
            if (kind == "res-body") {
                if (currentTraffic.res_body) {
                    let text = "";
                    if (currentTraffic.res_body.encode == "base64") {
                        let contentType = getContentType(currentTraffic.res_headers);
                        text = `data:${contentType};base64,${currentTraffic.res_body.value}`;
                    } else {
                        text = currentTraffic.res_body.value;
                    }
                    if (text) {
                        ClipboardJS.copy(text);
                    }
                }
            } else {
                fetch(getTrafficEndpoint(selectedTrafficId) + `?${kind}`)
                    .then(res => res.text())
                    .then(text => {
                        ClipboardJS.copy(text);
                    })
                    .catch(err => {
                        console.error(`Failed to copy traffic ${selectedTrafficId}`, err);
                    })

            }
        }

        function updateTabContent() {
            let innerHTML = "";

            if (!websocketAbortController.signal.aborted) {
                websocketAbortController.abort();
            }
            websocketAbortController = new AbortController();

            if (currentTraffic) {
                if (selectedTab === "req") {
                    innerHTML = tmplTrafficReq(currentTraffic);
                } else if (selectedTab == "res") {
                    if (currentTraffic.error) {
                        handleSelectedTab("err");
                        return;
                    } else {
                        innerHTML = tmplTrafficRes(currentTraffic);
                    }
                } else if (selectedTab == "ws") {
                    if (currentTraffic.websocket_id) {
                        handleSubscribeWebsocket(currentTraffic.websocket_id);
                    } else {
                        handleSelectedTab("res");
                        return;
                    }
                } else if (selectedTab == "err") {
                    if (currentTraffic.error) {
                        innerHTML = tmplError(currentTraffic.error);
                    } else {
                        handleSelectedTab("res");
                        return;
                    }
                }
            }
            if (currentTraffic?.error) {
                $tabs.querySelector(".tab[data-tab='res']").classList.add("hidden");
                $tabs.querySelector(".tab[data-tab='ws']").classList.add("hidden");
                $tabs.querySelector(".tab[data-tab='err']").classList.remove("hidden");
            } else if (currentTraffic?.websocket_id) {
                $tabs.querySelector(".tab[data-tab='res']").classList.remove("hidden");
                $tabs.querySelector(".tab[data-tab='ws']").classList.remove("hidden");
                $tabs.querySelector(".tab[data-tab='err']").classList.add("hidden");
            } else {
                $tabs.querySelector(".tab[data-tab='res']").classList.remove("hidden");
                $tabs.querySelector(".tab[data-tab='ws']").classList.add("hidden");
                $tabs.querySelector(".tab[data-tab='err']").classList.add("hidden");
            }
            $mainView.innerHTML = innerHTML;
        }

        function handleSubscribeWebsocket(id) {
            fetch(subscribeWebsocketEndpoint(id), { signal: websocketAbortController.signal })
                .then((res) => {
                    let buf = "";
                    let decoder = new TextDecoder();
                    let reader = res.body.getReader();
                    reader.read().then(function handleNDJson({ value, done }) {
                        if (done) {
                            buf = buf.trim();
                        } else {
                            let data = decoder.decode(value, { stream: true });
                            buf += data;
                        }
                        let lines = buf.split("\n");
                        for (let i = 0; i < lines.length - 1; i++) {
                            try {
                                const message = JSON.parse(lines[i]);
                                handleNewWebsocketMessage(message);
                            } catch { }
                        }
                        buf = lines[lines.length - 1];
                        return reader.read().then(handleNDJson);
                    });
                })
                .catch((err) => {
                    if (err.name === "AbortError") {
                        return;
                    }
                    console.error(`Failed to subscribe websocket ${id}`, err);
                });
        }

        /**
         * @param {WebsocketMessage} message
         */
        function handleNewWebsocketMessage(message) {
            let tmpl = '';
            if (message.error) {
                tmpl = tmplError(message.error);
            } else if (message.data) {
                tmpl = tmplWebsocketData(message.data);
            }
            $mainView.insertAdjacentHTML("beforeend", tmpl);
        }

        /**
         * @param {TrafficHead} trafficHead
         */
        function tmplTrafficTableRow(trafficHead) {
            return `
            <tr data-traffic-id="${trafficHead.id}">
                <td class="col-uri">${trafficHead.uri}</td>
                <td class="col-method">${trafficHead.method}</td>
                <td class="col-status">${trafficHead.status || ""}</td>
                <td class="col-type">${resolveResourceType(trafficHead)}</td>
                <td class="col-size">${formatSize(trafficHead.size)}</td>
                <td class="col-time">${formatTimeDelta(trafficHead.time)}</td>
            </tr>`;
        }

        /**
         * @param {Traffic} traffic
         */
        function tmplTrafficReq(traffic) {
            return [
                `<div class="firstline">${traffic.method} ${traffic.uri} ${traffic.req_version || ""}</div>`,
                tmplHaders(traffic.req_headers),
                tmplBody(traffic.req_body, traffic.req_headers),
            ].join("");
        }

        /**
         * @param {Traffic} traffic
         */
        function tmplTrafficRes(traffic) {
            return [
                `<div class="firstline">${[traffic.res_version || "", traffic.status, STATUS_CODES[traffic.status]].join(" ")}</div>`,
                tmplHaders(traffic.res_headers),
                tmplBody(traffic.res_body, traffic.res_headers),
            ].join("");
        }

        /**
         * @param {?Header[]} headers
         */
        function tmplHaders(headers) {
            return (headers || []).map(({ name, value }) =>
                `<div class="header-line"><span class="header-key">${name}</span>:&nbsp;<span class="header-value">${value}</span></div>`
            ).join("");
        }

        /**
         * @param {BodyData} body
         * @param {?Header[]} headers
         */
        function tmplBody(body, headers) {
            if (!body) return "";
            let { encode, value } = body;
            if (!value) return "";
            let contentType = getContentType(headers);
            if (encode == "utf8") {
                let language = highlightLanguage(contentType);
                const highlightedCode = hljs.getLanguage(language)
                    ? hljs.highlight(value, { language }).value
                    : hljs.highlightAuto(value).value;
                return `<div class="code-view"><pre><code>${highlightedCode}</code></pre></div>`;
            }
            if (encode == "base64") {
                if (
                    IMAGE_MIMES.includes(contentType)
                ) {
                    return `<div class="media-view"><img src="data:${contentType};base64,${value}" alt="image" style="max-width: 100%;"></div>`;
                } else if (AUDIO_MIMES.includes(contentType)) {
                    return `<div class="media-view"><audio controls style="max-width: 100%;"><source src="data:${contentType};base64,${value}" type="${contentType}"></audio></div>`;
                } else if (VIDEO_MIMES.includes(contentType)) {
                    return `<div class="media-view"><video controls style="max-width: 100%;"><source src="data:${contentType};base64,${value}" type="${contentType}"></video></div>`;
                } else {
                    let filename = "download.bin";
                    let disposition = getHeader(headers, "content-disposition")
                    if (/filename="([^"]+?)"/.test(disposition)) {
                        filename = disposition.match(/filename="(.*?)"/)[1] || filename;
                    }
                    let downloadUrl = URL.createObjectURL(base64ToBlob(value, contentType || "application/octet-stream"));
                    return `<div class="media-view"><a href="${downloadUrl}" download="${filename}">Download</a></div>`;
                }
            }
            return `<div class="code-view">${value}</div>`;
        }

        function tmplError(error) {
            if (!error) return "";
            return `<div class="error-view">${error}</div>`;
        }

        /**
         * @param {WebsocketData} data
         */
        function tmplWebsocketData(data) {
            let arrow = '';
            if (data.server_to_client) {
                arrow = '<i class="ws-recv">🠘</i>';
            } else {
                arrow = '<i class="ws-send">🠚</i>';
            }
            let date = formatDate(new Date(data.create));
            let value = data.body.value;
            if (data.body.encode == "base64") {
                value = `data:;base64,${value}`;
            }
            return `<div class="ws-msg">
                <small>${arrow}<span class="ws-date">${date}</span></small>
                <div class="code-view">
                    <pre><code>${value}</code></pre>
                </div>
            </div>`;
        }

        /**
         * @param {?Header[]} headers
         */
        function getContentType(headers) {
            return getHeader(headers, "content-type").split(';')[0].trim();
        }

        /**
         * @param {?Header[]} headers
         */
        function getHeader(headers, type) {
            return (headers || []).find(({ name }) => name.toLowerCase() === type)?.value || "";
        }

        /**
         * @param {TrafficHead} trafficHead
         */
        function resolveResourceType(trafficHead) {
            if (trafficHead.mime) {
                if (trafficHead.mime.startsWith("image/")) {
                    return "IMAGE";
                }
                if (trafficHead.mime.startsWith("audio/")) {
                    return "AUDIO";
                }
                if (trafficHead.mime.startsWith("video/")) {
                    return "VIDEO";
                }
                if (trafficHead.mime.indexOf("json") >= 0) {
                    return "JSON"
                }
                if (trafficHead.mime.indexOf("xml") >= 0) {
                    return "XML"
                }
                if (trafficHead.mime.indexOf("javascript") >= 0) {
                    return "JS"
                }
                if (trafficHead.mime.indexOf("css") >= 0) {
                    return "CSS"
                }
                if (trafficHead.mime.indexOf("html") >= 0) {
                    return "HTML"
                }
            } else if (trafficHead.status) {
                if (trafficHead.status == 101) {
                    return "WEBSOCKET"
                } else if (trafficHead.status == 304) {
                    return "NOTMODIFY"
                } else if (trafficHead.status >= 300 && trafficHead.status < 400) {
                    return "REDIRECT"
                }
            }
            return "";
        }

        /** 
         * @param {string} contentType
         */
        function highlightLanguage(contentType) {
            let value = contentType.startsWith('text/') || contentType.startsWith('application/')
                ? contentType.substring(contentType.indexOf('/') + 1)
                : '';

            return value.startsWith('x-') ? value.substring(2) : value;
        }

        function base64ToBlob(base64String, contentType) {
            const byteCharacters = atob(base64String);
            const byteArrays = [];

            for (let offset = 0; offset < byteCharacters.length; offset += 512) {
                const slice = byteCharacters.slice(offset, offset + 512);

                const byteNumbers = new Array(slice.length);
                for (let i = 0; i < slice.length; i++) {
                    byteNumbers[i] = slice.charCodeAt(i);
                }

                const byteArray = new Uint8Array(byteNumbers);
                byteArrays.push(byteArray);
            }

            return new Blob(byteArrays, { type: contentType });

        }

        function formatSize(bytes) {
            if (bytes === null) return "";
            if (bytes === 0) return "0";
            const prefix = ["b", "kb", "mb", "gb", "tb"];
            let i = 0;
            for (; i < prefix.length; i++) {
                if (Math.pow(1024, i + 1) > bytes) {
                    break;
                }
            }
            let precision;
            if (bytes % Math.pow(1024, i) === 0) precision = 0;
            else precision = 1;
            return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i];
        }

        function formatTimeDelta(milliseconds) {
            let time = milliseconds;
            if (time === null) return "";
            if (time === 0) return "0";
            const prefix = ["ms", "s", "min", "h"];
            const div = [1000, 60, 60];
            let i = 0;
            while (Math.abs(time) >= div[i] && i < div.length) {
                time = time / div[i];
                i++;
            }
            return Math.round(time) + prefix[i];
        }

        function formatDate(date) {
            const year = date.getFullYear();
            const month = date.getMonth() + 1; // months are 0-indexed
            const day = date.getDate();
            const hours = date.getHours();
            const minutes = date.getMinutes();
            const seconds = date.getSeconds();
            const milliseconds = date.getMilliseconds();

            const formattedMonth = month < 10 ? `0${month}` : month;
            const formattedDay = day < 10 ? `0${day}` : day;
            const formattedHours = hours < 10 ? `0${hours}` : hours;
            const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
            const formattedSeconds = seconds < 10 ? `0${seconds}` : seconds;

            return `${year}-${formattedMonth}-${formattedDay} ${formattedHours}:${formattedMinutes}:${formattedSeconds}.${milliseconds}`;
        }

        document.addEventListener("DOMContentLoaded", handleDOMContentLoaded);
    </script>
</body>

</html>